/*
 * Copyright (C) 2010-2018 Gordon Fraser, Andrea Arcuri and EvoSuite
 * contributors
 *
 * This file is part of EvoSuite.
 *
 * EvoSuite is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation, either version 3.0 of the License, or
 * (at your option) any later version.
 *
 * EvoSuite is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with EvoSuite. If not, see <http://www.gnu.org/licenses/>.
 */
package org.evosuite.symbolic.instrument;

import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;

import static org.evosuite.dse.util.Assertions.check;
import static org.evosuite.symbolic.instrument.ConcolicConfig.VM_FQ;
import static org.objectweb.asm.Opcodes.*;
import static org.objectweb.asm.Type.INT_TYPE;
import static org.objectweb.asm.Type.VOID_TYPE;

/*
    This class is taken and adapted from the DSC tool developed by Christoph Csallner.
    Link at :
    http://ranger.uta.edu/~csallner/dsc/index.html
 */

/**
 * Helper class
 * <p>
 * Methods to generate bytecodes that mutate the
 * operand stack
 * <p>
 * TODO: Handle all cases via local variables, via LocalVariablesSorter:
 * http://asm.ow2.org/asm30/javadoc/user/org/objectweb/asm/commons/LocalVariablesSorter.html
 *
 * @author csallner@uta.edu (Christoph Csallner)
 */
final class OperandStack {

    private final MethodVisitor mv;
    private static final String CALLER_STACK_PARAM = "CALLER_STACK_PARAM"; //$NON-NLS-1$
    private static final Type TYPE_OBJECT = Type.getObjectType("java/lang/Object"); //$NON-NLS-1$

    OperandStack(MethodVisitor mv) {
        this.mv = mv;
    }

    private String desc(Type returnType, Type... argumentTypes) {
        StringBuffer desc = new StringBuffer();
        desc.append("("); //$NON-NLS-1$
        for (Type type : argumentTypes)
            desc.append(type.getDescriptor());
        desc.append(")"); //$NON-NLS-1$
        desc.append(returnType.getDescriptor());
        return desc.toString();
    }

    /**
     * Push i onto our operand stack
     */
    void pushInt(int i) {
        switch (i) {
            case -1:
                mv.visitInsn(ICONST_M1);
                return;
            case 0:
                mv.visitInsn(ICONST_0);
                return;
            case 1:
                mv.visitInsn(ICONST_1);
                return;
            case 2:
                mv.visitInsn(ICONST_2);
                return;
            case 3:
                mv.visitInsn(ICONST_3);
                return;
            case 4:
                mv.visitInsn(ICONST_4);
                return;
            case 5:
                mv.visitInsn(ICONST_5);
                return;
        }
        if (i >= Byte.MIN_VALUE && i <= Byte.MAX_VALUE) {
            mv.visitIntInsn(BIPUSH, i);
            return;
        }
        if (i >= Short.MIN_VALUE && i <= Short.MAX_VALUE) {
            mv.visitIntInsn(SIPUSH, i);
            return;
        }
        mv.visitLdcInsn(i);
    }

    /**
     * ...
     * ==>
     * ..., s[0], s[1], ..., s[n]
     */

    void pushStrings(String... strings) {
        for (String string : strings) {
            mv.visitLdcInsn(string);
        }
    }

    /**
     * ..., b1, a1
     * ==>
     * ..., b1, a1, b1
     */
    void b1a1__b1a1b1() {
        /* ..., b1, a1*/
        mv.visitInsn(SWAP);
        /* ..., a1, b1*/
        mv.visitInsn(DUP_X1);
        /* ..., b1, a1, b1*/
    }

    /**
     * ..., b1, a2
     * ==>
     * ..., b1, a2, b1
     */
    void b1a2__b1a2b1() {
        /* ..., b1, a2 */
        mv.visitInsn(DUP2_X1);
        /* ..., a2, b1, a2 */
        mv.visitInsn(POP2);
        /* ..., a2, b1 */
        mv.visitInsn(DUP_X2);
        /* ..., b1, a2, b1 */
    }

    /**
     * ..., b2, a1
     * ==>
     * ..., b2, a1, b2
     */
    void b2a1__b2a1b2() {
        /* ..., b2, a1 */
        mv.visitInsn(DUP_X2);
        /* ..., a1, b2, a1 */
        mv.visitInsn(POP);
        /* ..., a1, b2 */
        mv.visitInsn(DUP2_X1);
        /* ..., b2, a1, b2 */
    }

    /**
     * ..., b2, a2
     * ==>
     * ..., b2, a2, b2
     */
    void b2a2__b2a2b2() {
        /* ..., b2, a2 */
        mv.visitInsn(DUP2_X2);
        /* ..., a2, b2, a2 */
        mv.visitInsn(POP2);
        /* ..., a2, b2 */
        mv.visitInsn(DUP2_X2);
        /* ..., b2, a2, b2 */
    }

    /**
     * Transform top of operand stack, assuming each operand is of category-1:
     * <p>
     * ..., c1, b1, a1
     * ==>
     * ..., c1, b1, a1, c1
     */
    void c1b1a1__c1b1a1c1() {
        /* ..., 2, 1, 0*/
        mv.visitInsn(DUP_X2);
        /* ..., 0, 2, 1, 0*/
        mv.visitInsn(POP);
        /* ..., 0, 2, 1 */
        mv.visitInsn(DUP_X2);
        /* ..., 1, 0, 2, 1 */
        mv.visitInsn(POP);
        /* ..., 1, 0, 2 */
        mv.visitInsn(DUP_X2);
        /* ..., 2, 1, 0, 2 */
    }

    /**
     * ..., c1, b1, a2
     * ==>
     * ..., c1, b1, a2, c1
     */
    void c1b1a2__c1b1a2c1() {
        /* ..., c1, b1, a2 */
        mv.visitInsn(DUP2_X2);
        /* ..., a2, c1, b1, a2 */
        mv.visitInsn(POP2);
        /* ..., a2, c1, b1 */
        mv.visitInsn(DUP2_X2);
        /* ..., c1, b1, a2, c1, b1 */
        mv.visitInsn(POP);
        /* ..., c1, b1, a2, c1 */
    }

    /**
     * ..., c1, b2, a1
     * ==>
     * ..., c1, b2, a1, c1
     */
    void c1b2a1__c1b2a1c1() {
        /* ..., c1, b2, a1 */
        mv.visitInsn(DUP_X2);                        // Form 2
        /* ..., c1, a1, b2, a1 */
        mv.visitInsn(POP);
        /* ..., c1, a1, b2 */
        mv.visitInsn(DUP2_X2);                    // Form 2
        /* ..., b2, c1, a1, b2 */
        mv.visitInsn(POP2);
        /* ..., b2, c1, a1 */
        mv.visitInsn(DUP2_X2);                    // Form 3
        /* ..., c1, a1, b2, c1, a1 */
        mv.visitInsn(POP);
        /* ..., c1, a1, b2, c1 */
        mv.visitInsn(DUP_X2);                        // Form 2
        /* ..., c1, a1, c1, b2, c1 */
        mv.visitInsn(POP);
        /* ..., c1, a1, c1, b2 */
        mv.visitInsn(DUP2_X2);                    // Form 2
        /* ..., c1, b2, a1, c1, b2 */
        mv.visitInsn(POP2);
        /* ..., c1, b2, a1, c1 */
    }

    /**
     * Pass a single parameter
     */
    void passCallerStackParam(Type argType, int paramNr, int calleeLocalsIndex) {
        /* Replace concrete complex type by java.lang.Object */
        Type type = argType;
        if (type.getSort() == Type.OBJECT || type.getSort() == Type.ARRAY)
            type = TYPE_OBJECT;

        pushInt(paramNr);
        pushInt(calleeLocalsIndex);
        String signature = desc(VOID_TYPE, type, INT_TYPE, INT_TYPE);
        mv.visitMethodInsn(INVOKESTATIC, VM_FQ, CALLER_STACK_PARAM, signature, false);
    }

    /**
     * Pass non-receiver-parameters one at a time,
     * right to left, downwards the operand stack
     */
    void passCallerStackParams(Type[] argTypes, boolean needThis) {
        if (argTypes == null || argTypes.length == 0)
            return;

        int calleeLocalsIndex = 0;
        for (Type type : argTypes)
            calleeLocalsIndex += type.getSize();
        if (needThis)
            calleeLocalsIndex += 1;

        int paramNr = argTypes.length - 1;    // work right to left


        if (argTypes[paramNr].getSize() == 1) {
            calleeLocalsIndex -= 1;
            mv.visitInsn(DUP);
            passCallerStackParam(argTypes[paramNr], paramNr, calleeLocalsIndex);
            if (argTypes.length == 1)
                return;
            paramNr -= 1;

            if (argTypes[paramNr].getSize() == 1) {
                calleeLocalsIndex -= 1;
                b1a1__b1a1b1();
                passCallerStackParam(argTypes[paramNr], paramNr, calleeLocalsIndex);
            } else if (argTypes[paramNr].getSize() == 2) {
                calleeLocalsIndex -= 2;
                b2a1__b2a1b2();
                passCallerStackParam(argTypes[paramNr], paramNr, calleeLocalsIndex);
            } else
                check(false);
        } else if (argTypes[paramNr].getSize() == 2) {
            calleeLocalsIndex -= 2;
            mv.visitInsn(DUP2);
            passCallerStackParam(argTypes[paramNr], paramNr, calleeLocalsIndex);
            if (argTypes.length == 1)
                return;
            paramNr -= 1;
            if (argTypes[paramNr].getSize() == 1) {
                calleeLocalsIndex -= 1;
                b1a2__b1a2b1();
                passCallerStackParam(argTypes[paramNr], paramNr, calleeLocalsIndex);
            } else if (argTypes[paramNr].getSize() == 2) {
                calleeLocalsIndex -= 2;
                b2a2__b2a2b2();
                passCallerStackParam(argTypes[paramNr], paramNr, calleeLocalsIndex);
            } else
                check(false);
        } else
            check(false);
    }
}